package org.mushroomdb.security;

import java.security.AccessController;
import java.security.Principal;
import java.sql.Types;
import java.util.Iterator;

import javax.security.auth.Subject;

import org.mushroomdb.catalog.Catalog;
import org.mushroomdb.catalog.Column;
import org.mushroomdb.catalog.Register;
import org.mushroomdb.catalog.Table;
import org.mushroomdb.engine.EvaluationEngine;
import org.mushroomdb.engine.EvaluationEngineFactory;
import org.mushroomdb.engine.ResultSet;
import org.mushroomdb.exception.EvaluationEngineException;
import org.mushroomdb.exception.RDBMSRuntimeException;
import org.mushroomdb.exception.TableConstraintException;
import org.mushroomdb.parser.ParserException;
import org.mushroomdb.query.Criteria;
import org.mushroomdb.query.impl.DeletionQuery;
import org.mushroomdb.query.impl.InsertionQuery;
import org.mushroomdb.query.impl.LiteralsSelectionQuery;
import org.mushroomdb.query.impl.QueryHelper;
import org.mushroomdb.query.impl.SelectionQuery;
import org.mushroomdb.query.impl.UpdateQuery;
import org.mushroomdb.security.jaas.MushroomPrincipal;
import org.mushroomdb.service.Service;
import org.mushroomdb.service.ServiceManager;

/**
 * @author Tomas
 * 
 *  
 */
public class SecurityService implements Service, SecurityKeys {

    private boolean initialized;

    //TODO:For testing purpose

    private ThreadLocal testUser= new ThreadLocal();

    public SecurityService() {

        this.initialized = false;
    }

    /**
     * Genera un nuevo registro en la tabla SYS_USER
     */

    public User createUser(String name, String password) {

        Catalog catalog = Catalog.getInstance();
        Table userTable = catalog.getTableByName(USER_TABLE_NAME);

        SelectionQuery selectionQuery = new SelectionQuery();
        Long userId = new Long(QueryHelper.getInstance().getNewId(userTable
                .getColumn(SecurityKeys.USER_ID_COLUMN_NAME)));
        selectionQuery.addProjection(userId);
        selectionQuery.addProjection(name);
        selectionQuery.addProjection(password);

        InsertionQuery insertionQuery;
        String insertionString = "INSERT INTO SYS_USER (ID,NAME,PASSWORD) VALUES ("
                + userId + "," + name + "," + password + ");";
        try {
            insertionQuery = new InsertionQuery(insertionString);
        } catch (ParserException e) {
            throw new RDBMSRuntimeException("El Usuario " + name
                    + " no pudo ser insertado");
        }
        int updateCount = EvaluationEngineFactory.getInstance().executeUpdate(
                insertionQuery);

        User user = null;
        if (updateCount == 1) {
            user = new User(userId, name, password);

        } else {
            throw new RDBMSRuntimeException("El Usuario " + name
                    + " no pudo ser insertado");
        }

        return user;

    }

    /**
     * Elimina un registro en la tabla SYS_USER
     */

    public void dropUser(User user) {

    	// FIXME!!! Remove related SYS_PERMISSION entries.
    	
        Catalog catalog = Catalog.getInstance();
        Table userTable = catalog.getTableByName(USER_TABLE_NAME);
        Column userIdColumn = userTable.getColumn("ID");
        
        SelectionQuery selectionQuery = new SelectionQuery();
        Criteria criteria = selectionQuery.getNewCriteria();
        criteria.addEqualsTo(userIdColumn,user.getId());

        DeletionQuery deletionQuery = new DeletionQuery(selectionQuery);

        int updateCount = EvaluationEngineFactory.getInstance().executeUpdate(
                deletionQuery);

        if (updateCount != 1) {
            throw new RDBMSRuntimeException("El Usuario " + user.getName()
                    + " no pudo ser eliminado");
        }
    }

    /**
     * Actualiza un registro en la tabla SYS_USER
     *  
     */
    public void updateUser(User user) {

        Catalog catalog = Catalog.getInstance();
        Table userTable = catalog.getTableByName(USER_TABLE_NAME);
        Column userIdColumn = userTable.getColumn(USER_ID_COLUMN_NAME);
        Column userNameColumn = userTable.getColumn(USER_NAME_COLUMN_NAME);
        Column userPassColumn = userTable.getColumn(USER_PASS_COLUMN_NAME);

        SelectionQuery selectionQuery = new SelectionQuery();
        Criteria criteria = selectionQuery.getNewCriteria();
        criteria.addEqualsTo(userIdColumn, user.getId());

        UpdateQuery updateQuery = new UpdateQuery(selectionQuery);
        updateQuery.setUpdate(userNameColumn, user.getName());
        updateQuery.setUpdate(userPassColumn, user.getPassword());

        int updateCount = EvaluationEngineFactory.getInstance().executeUpdate(
                updateQuery);
        if (updateCount != 1) {
            throw new RDBMSRuntimeException("El Usuario " + user.getName()
                    + " no pudo ser actualizado");
        }

    }

    /**
     * Devuelve el usuario de la tabla SYS_USER con el nombre pasado como
     * parametro
     */
    public User getUserByName(String name) {

        //FIXME: SACAR ESTO
        if ("mushroom".equals(name)) {
            User user = new User(new Long(0), "mushroom", "badger");
            return user;
        }
       
       
        Catalog catalog = Catalog.getInstance();

        Table userTable = catalog.getTableByName(USER_TABLE_NAME);
        Column userNameColumn = userTable.getColumn(USER_NAME_COLUMN_NAME);

        SelectionQuery selectionQuery = new SelectionQuery();
        Criteria criteria = selectionQuery.getNewCriteria();
        criteria.addEqualsTo(userNameColumn, name);

        ResultSet resultSet = EvaluationEngineFactory.getInstance()
                .executeQuery(selectionQuery);
        User user = null;

        try {
            //Tiene que devolver un solo registro porque el nombre es unique
            if (resultSet.hasNext()) {
                Register register = (Register) resultSet.next();
                Long userId = (Long) register.getValue(1);
                String userName = (String) register.getValue(2);
                String userPass = (String) register.getValue(3);

                if (!resultSet.hasNext()) {
                    user = new User(userId, userName, userPass);
                } else {
                    throw new TableConstraintException(
                            "Existe mas de un usuario con el nombre " + name);
                }
            }
        } catch (EvaluationEngineException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

        return user;

    }

    /**
     * Agrega un registro en la tabla SYS_PERMISSION con el "privilegio" y el
     * usuario pasados como parametros
     */

    public void grant(User user, Privilege permission, Table table) {
        Catalog catalog = Catalog.getInstance();
        Table permissionTable = catalog.getTableByName(PERMISSION_TABLE_NAME);

        SelectionQuery selectionQuery = new SelectionQuery();
        selectionQuery.addProjection(user.getId());
        selectionQuery.addProjection(table.getName());
        selectionQuery.addProjection(new Integer(permission.getCode()));

        //InsertionQuery insertionQuery = new InsertionQuery(selectionQuery);
        InsertionQuery insertionQuery;
        String insertionString = "INSERT INTO SYS_PERMISSION (USER_ID,TABLE_ID,PERMISSION_TYPE) VALUES ("
                + user.getId()
                + ","
                + table.getName()
                + ","
                + new Integer(permission.getCode()) + ");";
        try {
            insertionQuery = new InsertionQuery(insertionString);
        } catch (ParserException e) {
            throw new RDBMSRuntimeException("Al Usuario " + user.getName()
                    + " no le pudo ser adjudicado ningun privilegio");
        }
//        insertionQuery.setTable(permissionTable);

        EvaluationEngineFactory.getInstance().executeUpdate(insertionQuery);

    }

    /**
     * 
     * Elimina un registro de la tabla SYS_PERMISSION
     * 
     * @param user
     * @param permission
     * @param table
     */

    public void revoke(User user, Privilege permission, Table table) {

    	Catalog catalog = Catalog.getInstance();
        Table permissionTable = catalog.getTableByName(PERMISSION_TABLE_NAME);
        //USER_ID,TABLE_ID,PERMISSION_TYPE
        Column userIdColumn = permissionTable.getColumn("USER_ID");
        Column tableIdColumn = permissionTable.getColumn("TABLE_ID");
        Column permissionTypeColumn = permissionTable.getColumn("PERMISSION_TYPE");
        
        SelectionQuery selectionQuery = new SelectionQuery();
//        selectionQuery.addProjection(user.getId());
//        selectionQuery.addProjection(table.getName());
//        selectionQuery.addProjection(new Integer(permission.getCode()));
        Criteria criteria = selectionQuery.getNewCriteria();
        criteria.addEqualsTo(userIdColumn,user.getId());
        criteria.addEqualsTo(tableIdColumn,table.getName());
        criteria.addEqualsTo(permissionTypeColumn,new Integer(permission.getCode()));
        
        DeletionQuery deletionQuery = new DeletionQuery(selectionQuery);

        int updateCount = EvaluationEngineFactory.getInstance().executeUpdate(
                deletionQuery);

        if (updateCount < 1) {
            throw new RDBMSRuntimeException("El Permiso "
                    + permission.getName()
                    + " no pudo ser revocado para el usuario " + user.getName());
        }

    }

    /**
     * Verifica la existencia de un registro en la tabla SYS_PERMISSION sobre la
     * tabla, usuario y permiso pasados como parametros
     */
    public boolean checkPermission(User user, Privilege permission, Table table)
            throws AuthorizationException {

        Table permissionTable = Catalog.getInstance().getTableByName(
                PERMISSION_TABLE_NAME);
        Column userId = permissionTable
                .getColumn(PERMISSION_USER_ID_COLUMN_NAME);
        Column tableName = permissionTable
                .getColumn(PERMISSION_PERMISSION_OBJECT_ID_COLUMN_NAME);
        Column permissionType = permissionTable
                .getColumn(PERMISSION_PERMISSION_TYPE);

        SelectionQuery selectionQuery = new SelectionQuery();
        selectionQuery.addProjection(userId);
        selectionQuery.addProjection(tableName);
        selectionQuery.addProjection(permissionType);

        Criteria criteria = selectionQuery.getNewCriteria();
        criteria.addEqualsTo(userId, user.getId());
        criteria.addEqualsTo(tableName, table.getName());
        criteria.addEqualsTo(permissionType, new Integer(permission.getCode()));

        EvaluationEngine evaluationEngine = EvaluationEngineFactory
                .getInstance();
        ResultSet resultSet = evaluationEngine.executeQuery(selectionQuery);
        try {
            return resultSet.hasNext();
        } catch (EvaluationEngineException e) {
            return false;
        }

    }

    public User getUserFromContext() {
        if (this.initialized) {
            User testUser = (User) this.testUser.get();
            if (testUser != null) {
                return testUser;
            } else {
                String userName = "";
                User user = null;

                /** Gets the subject from the execution context */
                Subject subject = Subject.getSubject(AccessController
                        .getContext());

                if (subject != null) {
                    Iterator<Principal> iterator = subject.getPrincipals().iterator();
                    while (iterator.hasNext()) {
                    	Principal object = iterator.next();
                        if (object instanceof MushroomPrincipal) {
                            userName = object.getName();
                        }
                    }

                    SecurityService securityService = (SecurityService) ServiceManager
                            .getInstance().getService(SecurityService.class);
                    user = securityService.getUserByName(userName);

                }
                return user;
            }
        } else
            return null;
    }

    /**
     * Inicializa el servicio de seguridad. Si alguna tabla necesaria para
     * proveer este servicio no existiera, se crea.
     */
    public void initialize() {
        Catalog catalog = Catalog.getInstance();
        createUserTable(catalog);
        createPermissionTable(catalog);

        this.initialized = true;
    }

    /**
     * Inicializa la tabla SYS_S_OBJECT
     * 
     * @return
     */
    /*
     * private void createSObjectTable(Catalog catalog) { Table table =
     * catalog.getTableByName(S_OBJECT_TABLE_NAME);
     * 
     * 
     * if (table == null) {
     * 
     * Column tableId = new Column(S_OBJECT_TABLE_ID_COLUMN_NAME, Types.BIGINT);
     * tableId.setPk(true);
     * 
     * Column owner = new Column(S_OBJECT_OWNER_COLUMN_NAME, Types.BIGINT);
     * owner.setFk(true);
     * 
     * table = catalog.getBlankTable(); table.setName(S_OBJECT_TABLE_NAME);
     * table.addColumn(tableId); table.addColumn(owner);
     * 
     * catalog.createTable(table); } }
     */

    /**
     * Inicializa la tabla SYS_PERMISSION
     * 
     * @return
     */
    private void createPermissionTable(Catalog catalog) {
        Table table = catalog.getTableByName(PERMISSION_TABLE_NAME);

        /** Si la tabla no existe, se crea. */
        if (table == null) {

            Column userId = new Column(PERMISSION_USER_ID_COLUMN_NAME,
                    Types.BIGINT);
           // userId.setPk(true);
            userId.setFk(true);

            Column tableName = new Column(
                    PERMISSION_PERMISSION_OBJECT_ID_COLUMN_NAME, Types.CHAR, 20);
            //tableName.setPk(true);
            tableName.setFk(true);

            Column permissionType = new Column(PERMISSION_PERMISSION_TYPE,
                    Types.INTEGER);
            //permissionType.setPk(true);

            table = catalog.getBlankTable();
            table.setName(PERMISSION_TABLE_NAME);
            table.addColumn(userId);
            table.addColumn(tableName);
            table.addColumn(permissionType);

            catalog.createTable(table);
        }

    }

    /**
     * Inicializa la tabla SYS_USER
     * 
     * @return
     */
    private void createUserTable(Catalog catalog) {

        Table table = catalog.getTableByName(USER_TABLE_NAME);

        /** Si la tabla no existe, se crea. */
        if (table == null) {

            Column id = new Column(USER_ID_COLUMN_NAME, Types.BIGINT);
            id.setPk(true);
            id.setFk(true);

            Column usrName = new Column(USER_NAME_COLUMN_NAME, Types.CHAR, 20);
            usrName.setUnique(true);

            Column password = new Column(USER_PASS_COLUMN_NAME, Types.CHAR, 20);

            table = catalog.getBlankTable();
            table.setName(USER_TABLE_NAME);
            table.addColumn(id);
            table.addColumn(usrName);
            table.addColumn(password);

            catalog.createTable(table);
        }

    }

    /**
     * Genera un nuevo ID para la tabla SYS_USER
     * 
     * @return
     */
    private Long getNewUserId() {

        /** SELECT ID FROM SYS_INDEX; */
        Catalog catalog = Catalog.getInstance();
        Table userTable = catalog.getTableByName(USER_TABLE_NAME);
        Column idColumn = userTable.getColumn(USER_ID_COLUMN_NAME);

        SelectionQuery selectionQuery = new SelectionQuery();
        selectionQuery.addProjection(idColumn);

        /** Recorre los IDs y encuentra el mas grande */
        long maxId = 0;
        ResultSet resultSet = EvaluationEngineFactory.getInstance()
                .executeUnalterableQuery(selectionQuery);
        try {
            while (resultSet.hasNext()) {
                Register register = (Register) resultSet.next();
                long id = ((Long) register.getValue(1)).longValue();
                if (id > maxId) {
                    maxId = id;
                }
            }
        } catch (EvaluationEngineException e) {
            throw new RDBMSRuntimeException(e);
        } finally {
        	resultSet.close();
        }

        return new Long(maxId + 1);
    }

    public void setTestUser(User user) {
        this.testUser.set(user);
    }

}
